package database

import (
	"testing"

	"github.com/muhlemmer/gu"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestChangeWrite(t *testing.T) {
	type want struct {
		stmt string
		args []any
	}
	for _, test := range []struct {
		name   string
		change Change
		want   want
	}{
		{
			name:   "change",
			change: NewChange(NewColumn("table", "column"), "value"),
			want: want{
				stmt: "column = $1",
				args: []any{"value"},
			},
		},
		{
			name:   "change ptr to null",
			change: NewChangePtr[int](NewColumn("table", "column"), nil),
			want: want{
				stmt: "column = NULL",
				args: nil,
			},
		},
		{
			name:   "change ptr to value",
			change: NewChangePtr(NewColumn("table", "column"), gu.Ptr(42)),
			want: want{
				stmt: "column = $1",
				args: []any{42},
			},
		},
		{
			name: "multiple changes",
			change: NewChanges(
				NewChange(NewColumn("table", "column1"), "value1"),
				NewChangePtr[int](NewColumn("table", "column2"), nil),
				NewChange(NewColumn("table", "column3"), 123),
			),
			want: want{
				stmt: "column1 = $1, column2 = NULL, column3 = $2",
				args: []any{"value1", 123},
			},
		},
		{
			name:   "increment change",
			change: NewIncrementColumnChange(NewColumn("table", "counter")),
			want: want{
				stmt: "counter = counter + 1",
			},
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			var builder StatementBuilder
			err := test.change.Write(&builder)
			require.NoError(t, err)
			assert.Equal(t, test.want.stmt, builder.String())
			require.Len(t, builder.Args(), len(test.want.args))
			for i, arg := range test.want.args {
				assert.Equal(t, arg, builder.Args()[i])
			}
		})
	}
}

func TestChangeToStatement(t *testing.T) {
	type want struct {
		stmt string
		args []any
	}
	for _, test := range []struct {
		name           string
		prefillBuilder func(builder *StatementBuilder)
		change         *changeToStatement
		want           want
	}{
		{
			name: "change to statement",
			change: NewChangeToStatement(NewColumn("table", "column"), func(builder *StatementBuilder) {
				builder.WriteString("SELECT 1")
			}).(*changeToStatement),
			want: want{
				stmt: "column = (SELECT 1)",
				args: nil,
			},
		},
		{
			name: "change to statement with args",
			change: NewChangeToStatement(NewColumn("table", "column"), func(builder *StatementBuilder) {
				builder.WriteString("SELECT ")
				builder.WriteArg(42)
			}).(*changeToStatement),
			want: want{
				stmt: "column = (SELECT $1)",
				args: []any{42},
			},
		},
		{
			name: "change to statement with existing builder args",
			prefillBuilder: func(builder *StatementBuilder) {
				builder.WriteString("UPDATE table SET ")
				NewChanges(
					NewChange(NewColumn("table", "field1"), "asdf"),
					NewChangeToNull(NewColumn("table", "field2")),
					NewChangeToColumn(NewColumn("table", "field3"), NewColumn("table", "field4")),
				).Write(builder)
				builder.WriteString(", ")
			},
			change: NewChangeToStatement(NewColumn("table", "column"), func(builder *StatementBuilder) {
				builder.WriteString("SELECT ")
				builder.WriteArg(42)
				builder.WriteString(" FROM other_table WHERE ")
				NewBooleanCondition(NewColumn("table", "id"), true).Write(builder)
			}).(*changeToStatement),
			want: want{
				stmt: "UPDATE table SET field1 = $1, field2 = NULL, field3 = table.field4, column = (SELECT $2 FROM other_table WHERE table.id = $3)",
				args: []any{"asdf", 42, true},
			},
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			var builder StatementBuilder
			if test.prefillBuilder != nil {
				test.prefillBuilder(&builder)
			}
			test.change.Write(&builder)
			assert.Equal(t, test.want.stmt, builder.String())
			assert.Equal(t, builder.Args(), test.want.args)
		})
	}
}

func TestCTEChange(t *testing.T) {
	type want struct {
		stmt string
		args []any
	}
	for _, test := range []struct {
		name           string
		prefillBuilder func(builder *StatementBuilder)
		afterCTE       func(builder *StatementBuilder)
		change         *cteChange
		want           want
	}{
		{
			name: "only CTE change",
			change: NewCTEChange(
				func(builder *StatementBuilder) {
					builder.WriteString("SELECT 1 AS value")
				},
				nil,
			).(*cteChange),
			want: want{
				stmt: "WITH cte AS (SELECT 1 AS value) ",
				args: nil,
			},
		},
		{
			name: "with existing CTE",
			prefillBuilder: func(builder *StatementBuilder) {
				builder.WriteString("existing_cte AS (SELECT ")
				builder.WriteArg(42)
				builder.WriteString(" AS reason), ")
			},
			change: NewCTEChange(
				func(builder *StatementBuilder) {
					builder.WriteString("SELECT ")
					Columns{
						NewColumn("table", "column1"),
						NewColumn("table", "column2"),
					}.WriteQualified(builder)
					builder.WriteString(", ")
					builder.WriteArgs("asdf", 123, false, NullInstruction)
				},
				nil,
			).(*cteChange),
			want: want{
				stmt: "WITH existing_cte AS (SELECT $1 AS reason), cte AS (SELECT table.column1, table.column2, $2, $3, $4, NULL) ",
				args: []any{42, "asdf", 123, false},
			},
		},
		{
			name: "CTE change with after CTE statements",
			change: NewCTEChange(
				func(builder *StatementBuilder) {
					builder.WriteString("SELECT ")
					builder.WriteArg(1)
					builder.WriteString(" AS value")
				},
				nil,
			).(*cteChange),
			afterCTE: func(builder *StatementBuilder) {
				builder.WriteString("SELECT * FROM cte ")
			},
			want: want{
				stmt: "WITH cte AS (SELECT $1 AS value) SELECT * FROM cte ",
				args: []any{1},
			},
		},
		{
			name: "CTE change with column change after CTE",
			change: NewCTEChange(
				func(builder *StatementBuilder) {
					builder.WriteString("SELECT ")
					builder.WriteArg(1)
					builder.WriteString(" AS value")
				},
				func(name string) Change {
					return NewChangeToStatement(NewColumn("table", "field"), func(builder *StatementBuilder) {
						builder.WriteString("SELECT * FROM ")
						builder.WriteString(name)
						builder.WriteString(" WHERE ")
						NewColumnCondition(NewColumn(name, "value"), NewColumn("table", "value")).Write(builder)
					})
				},
			).(*cteChange),
			afterCTE: func(builder *StatementBuilder) {
				builder.WriteString("UPDATE test SET ")
			},
			want: want{
				stmt: "WITH cte AS (SELECT $1 AS value) UPDATE test SET field = (SELECT * FROM cte WHERE cte.value = table.value)",
				args: []any{1},
			},
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			var builder StatementBuilder
			builder.WriteString("WITH ")
			if test.prefillBuilder != nil {
				test.prefillBuilder(&builder)
			}

			builder.WriteString("cte AS (")
			test.change.SetName("cte")
			test.change.WriteCTE(&builder)
			builder.WriteString(") ")

			if test.afterCTE != nil {
				test.afterCTE(&builder)
			}
			test.change.Write(&builder)

			assert.Equal(t, test.want.stmt, builder.String())
			assert.Equal(t, builder.Args(), test.want.args)
		})
	}
}
